﻿using MongoDB.Bson;
using MongoDB.Driver;
using Sustainalytics.Admin.Synchronization.Utility;
using Sustainalytics.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Sustainalytics.DataLayer
{
    public class EventsRepository : IEventsRepository
    {
        private MongoClient Client { get; set; }
        private IMongoDatabase Database { get; set; }

        private EventsRepository(string connectionString)
        {
            // DB address
            var mongoUrl = MongoUrl.Create(connectionString);

            if (string.IsNullOrWhiteSpace(mongoUrl.DatabaseName))
            {
                throw new ArgumentException("No database specified in the connection string!");
            }

            // Mongo DB client
            Client = new MongoClient(mongoUrl);

            // Mongo DB
            Database = Client.GetDatabase(mongoUrl.DatabaseName);
        }

        public static IEventsRepository GetEventsRepository(string connectionString)
        {
            return new EventsRepository(connectionString);
        }

        public List<Event> GetEvents(EventsFilter filter, int skip, int take, string[] orderBy, bool asc)
        {
            var collection = this.Database.GetCollectionByNameType<Event>();

            var query = GetMongoQuery(filter);

            IFindFluent<Event, Event> cursor;
            cursor = collection.Find(query);

            SortDefinition<Event> mongoSortBy = null;
            if (orderBy != null)
            {
                foreach (var order in orderBy)
                {
                    mongoSortBy = asc ? mongoSortBy.Ascending(order) : mongoSortBy.Descending(order);
                }
            }

            if (mongoSortBy != null)
            {
                cursor.Sort(mongoSortBy);
            }

            cursor.Skip(skip).Limit(take);

            return cursor.ToList();
        }

        public long GetEventsCount(EventsFilter filter)
        {
            var collection = this.Database.GetCollectionByNameType<Event>();
            var query = GetMongoQuery(filter);
            return collection.Count(query);
        }

        public Event GetEvent(Guid eventId)
        {
            return Database.GetCollection<Event>(typeof(Event).Name).AsQueryable().FirstOrDefault(x => x.Id == eventId);
        }

        public IEnumerable<Event> GetEventsByIds(IEnumerable<Guid> eventsIds)
        {
            if (eventsIds == null)
            {
                throw new ArgumentNullException("eventsIds");
            }

            var eventsItemsQuery = Builders<Event>.Filter.In(n => n.Id, eventsIds);

            return Database.GetCollection<Event>(typeof(Event).Name).Find(eventsItemsQuery).ToList();
        }

        public void AddEvent(Event eventObj)
        {
            if (eventObj == null)
            {
                throw new ArgumentNullException("eventObj");
            }

            Database.GetCollection<Event>(typeof(Event).Name).InsertOne(eventObj);
        }

        public void UpdateEvent(Event eventObj)
        {
            if (eventObj == null)
            {
                throw new ArgumentNullException("eventObj");
            }

            Database.GetCollection<Event>(typeof(Event).Name).ReplaceOne(x=> x.Id == eventObj.Id, eventObj, new UpdateOptions { IsUpsert = true });
        }

        public void DeleteEvent(Guid eventId)
        {
            Database.GetCollection<Event>(typeof(Event).Name).DeleteMany(Builders<Event>.Filter.Eq(n => n.Id, eventId));
        }

        public void DeleteMultipleEvents(IEnumerable<Guid> eventItems)
        {
            if (eventItems == null)
            {
                throw new ArgumentNullException("eventItems");
            }

            Database.GetCollection<Event>(typeof(Event).Name).DeleteMany(Builders<Event>.Filter.In(n=> n.Id, eventItems));
        }

        public void UpdateMultipleEvents(IEnumerable<Guid> eventItems, EventStatus status)
        {
            if (eventItems == null)
            {
                throw new ArgumentNullException("eventItems");
            }

            var query = Builders<Event>.Filter.In(n => n.Id, eventItems);
            var update = Builders<Event>.Update.Set(x=> x.Status, status);
            Database.GetCollection<Event>(typeof(Event).Name).UpdateMany(query, update);
        }

        #region private helpers

        private static BsonRegularExpression GetSearchEventsRegex(string searchText, BsonRegularExpression regex)
        {
            Guard.ArgumentNotNullOrEmpty(searchText, "searchText");
            if (regex != null)
                return regex;
            var text = Regex.Escape(searchText);
            return new BsonRegularExpression(string.Format(".*{0}.*", text), "i");
        }

        private static FilterDefinition<Event> GetMongoQuery(EventsFilter filter)
        {
            var query = new List<FilterDefinition<Event>>();

            if (filter != null && filter.Sponsorship.HasValue)
            {
                query.Add(Builders<Event>.Filter.Eq(x => x.Sponsorship, filter.Sponsorship.Value));
            }

            BsonRegularExpression regex = null;
            if (filter != null && !String.IsNullOrWhiteSpace(filter.SearchText))
            {
                regex = GetSearchEventsRegex(filter.SearchText, regex);
                query.Add(Builders<Event>.Filter.Or(Builders<Event>.Filter.Regex(x => x.Name, regex), (Builders<Event>.Filter.Regex(x => x.Body, regex))));
            }

            /* es = element start date
             * ee = element end date
             * fs = filter start date
             * fe = filter end date
            */

            if (filter != null && filter.IsPublished)
            {
                if (filter.StartDateBegin != null && filter.StartDateEnd != null)
                {
                    query.Add(
                        Builders<Event>.Filter.Or(
                            //es <= fs && ee <= fe && ee >= fs && es < fe
                            Builders<Event>.Filter.And(
                                Builders<Event>.Filter.Lte(x => x.StartDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Lte(x => x.EndDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published)
                            ),
                            //es <= fs && ee >= fe && ee >= fs && es < fe
                            Builders<Event>.Filter.And(
                                Builders<Event>.Filter.Lte(x => x.StartDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published)
                            ),
                            //es >= fs && ee <= fe && ee >= fs && es < fe
                            Builders<Event>.Filter.And(
                                Builders<Event>.Filter.Gte(x => x.StartDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Lte(x => x.EndDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published)
                           ),
                           //es >= fs && ee >= fe && ee >= fs && es < fe
                           Builders<Event>.Filter.And(
                                Builders<Event>.Filter.Gte(x => x.StartDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Gte(x => x.EndDate, filter.StartDateBegin),
                                Builders<Event>.Filter.Lt(x => x.StartDate, filter.StartDateEnd),
                                Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published))
                        ));
                }
                else
                {
                    query.Add(Builders<Event>.Filter.And(
                        Builders<Event>.Filter.Eq(x => x.Status, EventStatus.Published),
                        Builders<Event>.Filter.Gt(x => x.StartDate, DateTime.UtcNow.AddMonths(-3))));
                }
            }
            return Builders<Event>.Filter.And(query);
        }

        #endregion
    }
}
